<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="utf-8" />
    <title>OLED UI 菜单结构编辑器 & C代码生成器</title>
    <style>
        :root {
            --theme: #007bff;
            --theme-dark: #0056b3;
            --radius: 6px;
            --shadow: 0 2px 6px rgba(0, 0, 0, .08);
            --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial;
        }

        * {
            box-sizing: border-box;
            font-family: var(--font);
        }

        details>summary {
            cursor: pointer;
            user-select: none;
            list-style: none;
            /* 隐藏默认三角 */
            display: flex;
            align-items: center;
        }

        details>summary::-webkit-details-marker {
            display: none;
        }

        details>summary::before {
            content: "▶";
            font-size: 10px;
            margin-right: 6px;
            transition: transform 0.15s;
        }

        details[open]>summary::before {
            transform: rotate(90deg);
        }

        body {
            margin: 0;
            background: #f4f6fa;
            display: flex;
            flex-direction: column;
            height: 100vh;
        }

        header {
            background: #fff;
            box-shadow: var(--shadow);
            padding: 12px 24px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        header h2 {
            font-size: 20px;
            margin: 0;
            color: #333;
        }

        .steps {
            display: flex;
            gap: 12px;
            font-size: 14px;
            color: #666;
        }

        .steps span {
            display: flex;
            align-items: center;
            gap: 6px;
        }

        .steps .num {
            background: var(--theme);
            color: #fff;
            border-radius: 50%;
            width: 22px;
            height: 22px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            font-size: 12px;
        }

        main {
            flex: 1;
            display: flex;
            gap: 20px;
            padding: 20px;
            overflow: hidden;
        }

        .left-tree,
        .left-props,
        .right {
            background: #fff;
            border-radius: var(--radius);
            box-shadow: var(--shadow);
            padding: 24px;
            display: flex;
            flex-direction: column;
        }

        .left-tree {
            width: 320px;
            max-width: 320px;
        }

        .left-props {
            width: 320px;
            max-width: 320px;
        }

        .right {
            flex: 1;
            flex-direction: row;
        }

        .preview-box {
            flex: 1;
            display: flex;
            flex-direction: column;
        }

        label {
            font-weight: 600;
            font-size: 14px;
            margin-bottom: 6px;
            display: block;
        }

        input[type=text],
        input[type=number],
        select,
        input[type=file] {
            width: 100%;
            padding: 8px 10px;
            border: 1px solid #ccd0d5;
            border-radius: var(--radius);
            font-size: 14px;
        }

        textarea {
            width: 100%;
            flex: 1;
            resize: none;
            border: 1px solid #ccd0d5;
            border-radius: var(--radius);
            padding: 10px;
            font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
            font-size: 13px;
            line-height: 1.45;
        }

        button {
            padding: 8px 12px;
            border-radius: var(--radius);
            border: none;
            font-size: 14px;
            cursor: pointer;
            transition: .2s;
            margin: 4px 0;
        }

        .btn-primary {
            background: var(--theme);
            color: #fff;
        }

        .btn-primary:hover {
            background: var(--theme-dark);
        }

        .btn-secondary {
            background: #e9ecef;
            color: #212529;
        }

        .btn-secondary:hover {
            background: #dee2e6;
        }

        button:disabled {
            opacity: .55;
            cursor: not-allowed;
        }

        .tree {
            border: 1px solid #ccd0d5;
            border-radius: var(--radius);
            padding: 12px;
            height: 280px;
            overflow-y: auto;
            background: #fafafa;
            margin-bottom: 10px;
        }

        .node {
            cursor: pointer;
            padding: 4px 6px;
            border-radius: 3px;
        }

        .node:hover {
            background: #e0e0e0;
        }

        .node.selected {
            background: #cce5ff;
        }

        .section {
            margin-bottom: 10px;
        }

        .move-buttons {
            display: flex;
            gap: 10px;
        }

        #copyTip {
            display: inline-block;
            margin-left: 8px;
            font-size: 12px;
            color: var(--theme);
            opacity: 0;
            transition: .3s;
        }

        #copyTip.show {
            opacity: 1;
        }
    </style>
</head>

<body>
    <header>
        <h2>OLED UI 菜单结构编辑器 & C代码生成器</h2>
        <div class="steps">
            <span><span class="num">1</span> 编辑结构</span>
            <span><span class="num">2</span> 生成JSON & C代码</span>
            <span><span class="num">3</span> 复制 / 下载</span>
        </div>
    </header>

    <main>
        <!-- 最左侧：菜单结构树 -->
        <section class="left-tree">
            <label>菜单结构树（点击选中节点）</label>
            <div class="tree" id="tree"></div>

            <!-- ✅ 搬到这里 -->
            <div class="section">
                <button class="btn-primary" onclick="generate_all(),extract_chars()">生成全部内容</button>
                <input type="file" id="fileInput" style="display:none" onchange="import_json(event)" />
                <button class="btn-primary" onclick="document.getElementById('fileInput').click()">导入JSON</button>
            </div>

            <div class="section">
                <button class="btn-secondary" onclick="copy_code('json')">复制JSON</button>
                <button class="btn-secondary" onclick="copy_code('c')">复制C代码</button>
                <span id="copyTip"></span>
            </div>

            <!-- 操作按钮组 -->
            <div class="section">
                <button class="btn-primary" onclick="add_sibling()">添加同级菜单</button>
                <button class="btn-primary" onclick="add_child()">添加子菜单</button>
                <button class="btn-secondary" onclick="remove_node()">删除节点</button>
            </div>

            <div class="section move-buttons">
                <button class="btn-secondary" onclick="move_node_up()">上移节点</button>
                <button class="btn-secondary" onclick="move_node_down()">下移节点</button>
            </div>

            <div class="section" style="margin-top:auto;">
                <label>当前使用的字符</label>
                <div style="border:1px solid #ccd0d5;border-radius:6px;padding:10px;background:#fafafa;font-size:13px;">
                    <div><strong>汉字：</strong><span id="used_chinese" style="color:#007bff;">无</span></div>
                    <div style="margin-top:6px;"><strong>英文字符：</strong><span id="used_ascii"
                            style="color:#28a745;">无</span></div>
                    <div style="margin-top:8px;">
                        <button class="btn-secondary" onclick="copy_chinese()">复制汉字</button>
                        <button class="btn-secondary" onclick="copy_ascii()">复制英文字符</button>
                    </div>
                </div>
            </div>
        </section>

        <!-- 中间：参数设置 -->
        <section class="left-props">
            <label>节点属性</label>
            <div id="props"></div>
        </section>

        <!-- 右侧：JSON 和 C 预览 -->
        <section class="right">
            <div class="preview-box">
                <label>JSON 预览</label>
                <textarea id="jsonPreview" readonly placeholder="点击“生成全部内容”后，JSON将显示在这里…"></textarea>
            </div>
            <div class="preview-box">
                <label>C 代码预览</label>
                <textarea id="cPreview" readonly placeholder="点击“生成全部内容”后，C代码将显示在这里…"></textarea>
            </div>
        </section>
    </main>

    <script>
        /* ================= 编辑器部分变量与函数 ================= */
        let root = { name: "root", type: "UI_COMP_NONE", children: [] };
        let selected_node = root;
        let generated_json = "";
        let generated_c_code = "";

        function render_tree(node, parent_el, depth = 0) {
            const hasChildren = node.children && node.children.length > 0;
            const isGroup = node.type === "UI_COMP_NONE";

            // 外层容器
            const wrapper = document.createElement("div");
            wrapper.style.marginLeft = (depth * 12) + "px";
            wrapper.style.marginTop = "2px";

            if (isGroup && hasChildren) {
                // 可折叠组
                const details = document.createElement("details");
                details.open = false;   // 初始都收起来，由 rerender() 再打开

                const summary = document.createElement("summary");
                summary.className = "node";
                summary.textContent = node.name + " (组)";
                summary.onclick = (e) => {
                    e.stopPropagation(); // 避免折叠时触发选中
                    select_node(node, summary);
                };

                details.appendChild(summary);
                wrapper.appendChild(details);

                // 递归子节点
                const childrenBox = document.createElement("div");
                childrenBox.style.marginTop = "4px";
                node.children.forEach(ch => render_tree(ch, childrenBox, depth + 1));
                details.appendChild(childrenBox);

                parent_el.appendChild(wrapper);
            } else {
                // 叶子或滑块等
                const el = document.createElement("div");
                el.className = "node";
                el.style.paddingLeft = "6px";
                el.textContent = node.name + (node.type === "UI_COMP_SLIDER" ? " (滑块)" :
                    node.type === "UI_COMP_SELECTOR" ? " (选择器)" :
                        node.type === "UI_COMP_FILE_BROWSER" ? " (文件浏览器)" : "");
                el.onclick = () => select_node(node, el);
                wrapper.appendChild(el);
                parent_el.appendChild(wrapper);
            }
        }

        function select_node(node, el) {
            selected_node = node;
            document.querySelectorAll(".node").forEach(n => n.classList.remove("selected"));
            el.classList.add("selected");
            render_props();
        }

        function render_props() {
            const p = document.getElementById("props");
            p.innerHTML = `
    <label>名称</label><input id="name" value="${selected_node.name}" onchange="selected_node.name=this.value; rerender();"/>
    <label>类型</label>
    <select id="type" onchange="selected_node.type=this.value; apply_default_data(); rerender();">
      <option value="UI_COMP_NONE" ${selected_node.type === "UI_COMP_NONE" ? "selected" : ""}>组</option>
      <option value="UI_COMP_SLIDER" ${selected_node.type === "UI_COMP_SLIDER" ? "selected" : ""}>滑块</option>
      <option value="UI_COMP_SELECTOR" ${selected_node.type === "UI_COMP_SELECTOR" ? "selected" : ""}>选择器</option>
      <option value="UI_COMP_FILE_BROWSER" ${selected_node.type === "UI_COMP_FILE_BROWSER" ? "selected" : ""}>文件浏览器</option>
    </select>
    ${selected_node.type === "UI_COMP_SLIDER" ? `
      <label>最小值</label><input type="number" value="${selected_node.comp_data?.min ?? 0}" onchange="ensure_comp_data(); selected_node.comp_data.min=+this.value;"/>
      <label>最大值</label><input type="number" value="${selected_node.comp_data?.max ?? 100}" onchange="ensure_comp_data(); selected_node.comp_data.max=+this.value;"/>
      <label>步长</label><input type="number" value="${selected_node.comp_data?.step ?? 10}" onchange="ensure_comp_data(); selected_node.comp_data.step=+this.value;"/>
      <label>延时(ms)</label><input type="number" value="${selected_node.comp_data?.timeout_ms ?? 1500}" onchange="ensure_comp_data(); selected_node.comp_data.timeout_ms=+this.value;"/>
      <label>单位</label><input value="${selected_node.comp_data?.unit ?? "%"}" onchange="ensure_comp_data(); selected_node.comp_data.unit=this.value;"/>
      <label>回调函数</label><input value="${selected_node.handler ?? ""}" onchange="selected_node.handler=this.value;"/>
      <label>变量头文件路径</label><input value="${selected_node.var_header ?? ""}" onchange="selected_node.var_header=this.value;"/>
      <label>变量名</label><input value="${selected_node.var_name ?? ""}" onchange="selected_node.var_name=this.value;"/>
    ` : ""}
    ${selected_node.type === "UI_COMP_SELECTOR" ? `
      <label>选项（逗号分隔）</label><input value="${selected_node.comp_data?.options?.join(", ") ?? "default"}" onchange="ensure_comp_data(); selected_node.comp_data.options=this.value.split(',').map(s=>s.trim());"/>
      <label>延时(ms)</label><input type="number" value="${selected_node.comp_data?.timeout_ms ?? 1500}" onchange="ensure_comp_data(); selected_node.comp_data.timeout_ms=+this.value;"/>
      <label>回调函数</label><input value="${selected_node.handler ?? ""}" onchange="selected_node.handler=this.value;"/>
      <label>变量头文件路径</label><input value="${selected_node.var_header ?? ""}" onchange="selected_node.var_header=this.value;"/>
      <label>变量名</label><input value="${selected_node.var_name ?? ""}" onchange="selected_node.var_name=this.value;"/>
    ` : ""}
    ${selected_node.type === "UI_COMP_FILE_BROWSER" ? `
      <label>回调函数</label><input value="${selected_node.handler ?? ""}" onchange="selected_node.handler=this.value;"/>
    ` : ""}
    ${selected_node.type === "UI_COMP_NONE" ? `
      <label>回调函数</label><input value="${selected_node.handler ?? ""}" onchange="selected_node.handler=this.value;"/>
    ` : ""}
  `;
        }

        function apply_default_data() {
            if (selected_node.type === "UI_COMP_SLIDER") {
                selected_node.comp_data = { min: 0, max: 100, step: 10, timeout_ms: 1500, unit: "%" };
            } else if (selected_node.type === "UI_COMP_SELECTOR") {
                selected_node.comp_data = { options: ["default"], timeout_ms: 1500 };
            } else if (selected_node.type === "UI_COMP_FILE_BROWSER") {
                selected_node.comp_data = null;
            } else {
                selected_node.comp_data = null;
            }
            render_props();
        }

        function ensure_comp_data() {
            if (!selected_node.comp_data) selected_node.comp_data = {};
        }

        function rerender() {
            // 1. 记录当前展开的路径
            const opened = new Set();
            document.querySelectorAll('details[open]').forEach(d => {
                // 用 summary 的文字当唯一 key（简单够用）
                opened.add(d.querySelector('summary').textContent);
            });

            // 2. 重画整棵树
            const t = document.getElementById("tree");
            t.innerHTML = "";
            render_tree(root, t);

            // 3. 恢复展开状态
            document.querySelectorAll('details').forEach(d => {
                if (opened.has(d.querySelector('summary').textContent)) {
                    d.open = true;
                }
            });
        }

        function add_sibling() {
            if (selected_node === root) return alert("根节点不能加同级");
            const parent = find_parent(root, selected_node);
            if (!parent) return;
            const n = { name: "menu", type: "UI_COMP_NONE", children: [] };
            parent.children.push(n);
            selected_node = n;
            rerender();
        }

        function add_child() {
            const n = { name: "menu", type: "UI_COMP_NONE", children: [] };
            selected_node.children.push(n);
            selected_node = n;
            rerender();
        }

        function remove_node() {
            if (selected_node === root) return alert("无法删除根节点");
            const parent = find_parent(root, selected_node);
            if (!parent) return;
            parent.children = parent.children.filter(c => c !== selected_node);
            selected_node = root;
            rerender();
        }

        function move_node_up() {
            if (selected_node === root) return alert("根节点无法移动");
            const parent = find_parent(root, selected_node);
            if (!parent) return;
            const index = parent.children.indexOf(selected_node);
            if (index > 0) {
                [parent.children[index], parent.children[index - 1]] = [parent.children[index - 1], parent.children[index]];
                rerender();
            } else {
                alert("已经是最顶部了");
            }
        }

        function move_node_down() {
            if (selected_node === root) return alert("根节点无法移动");
            const parent = find_parent(root, selected_node);
            if (!parent) return;
            const index = parent.children.indexOf(selected_node);
            if (index < parent.children.length - 1) {
                [parent.children[index], parent.children[index + 1]] = [parent.children[index + 1], parent.children[index]];
                rerender();
            } else {
                alert("已经是最底部了");
            }
        }

        function find_parent(node, target) {
            if (node.children && node.children.includes(target)) return node;
            for (let c of node.children || []) {
                const p = find_parent(c, target);
                if (p) return p;
            }
            return null;
        }

        function import_json(e) {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = () => {
                try {
                    const data = JSON.parse(reader.result);
                    function fix_children(node) {
                        if (!Array.isArray(node.children)) node.children = [];
                        node.children.forEach(fix_children);
                    }
                    fix_children(data);
                    root = data;
                    selected_node = root;
                    rerender();
                } catch (err) {
                    alert("JSON 解析失败：" + err.message);
                }
            };
            reader.readAsText(file);
        }

        /* ================= C代码生成部分 ================= */
        function buildVar(idxPath) { return "ui_" + idxPath.join("_"); }

        function collectDeclares(node, idxPath, decls, childrenDecls, sliderDecls, selectorDecls, fbDecls) {
            const _var = buildVar(idxPath);
            if (idxPath.length === 1) decls.push(`const UiMenuItem ${_var};`);
            else decls.push(`static const UiMenuItem ${_var};`);
            if (node.child_num) childrenDecls.push(`static const UiMenuItem *const ${_var}_children[];`);
            if (node.type === "UI_COMP_SLIDER") sliderDecls.push(`static const UiSlider ${_var}_slider;`);
            else if (node.type === "UI_COMP_SELECTOR") selectorDecls.push(`static const UiSelector ${_var}_selector;`);
            else if (node.type === "UI_COMP_FILE_BROWSER") fbDecls.push(`static const UiFileBrowser ${_var}_fb;`);
            (node.children || []).forEach((ch, idx) => collectDeclares(ch, [...idxPath, idx + 1], decls, childrenDecls, sliderDecls, selectorDecls, fbDecls));
        }

        function collectDefines(node, idxPath, lines) {
            const _var = buildVar(idxPath);
            if (node.type === "UI_COMP_SLIDER") {
                const d = node.comp_data;
                lines.push(`static UiSliderVars ${_var}_slider_var={0};`);
                lines.push(`static const UiSlider ${_var}_slider={`);
                lines.push(`    .min=${d.min},`);
                lines.push(`    .max=${d.max},`);
                lines.push(`    .step=${d.step},`);
                lines.push(`    .timeout_ms=${d.timeout_ms},`);
                lines.push(`    .unit="${d.unit}",`);
                lines.push(`    .var=&${_var}_slider_var,`);
                lines.push(`};`, "");
            } else if (node.type === "UI_COMP_SELECTOR") {
                const d = node.comp_data;
                lines.push(`static UiSelectorVars ${_var}_selector_var={0};`);
                const opts = d.options.map(o => `"${o}"`).join(",");
                lines.push(`static const UiSelector ${_var}_selector={`);
                lines.push(`    .options=(const char *const[]){${opts}},`);
                lines.push(`    .option_num=${d.options.length},`);
                lines.push(`    .timeout_ms=${d.timeout_ms},`);
                lines.push(`    .var=&${_var}_selector_var,`);
                lines.push(`};`, "");
            } else if (node.type === "UI_COMP_FILE_BROWSER") {
                lines.push(`static UiFileBrowserVars ${_var}_fb_var={0};`);
                lines.push(`static const UiFileBrowser ${_var}_fb={`);
                lines.push(`    .var=&${_var}_fb_var,`);
                lines.push(`};`, "");
            }
            if (node.child_num) {
                lines.push(`static const UiMenuItem *const ${_var}_children[]={`);
                node.children.forEach((ch, idx) => lines.push(`    &${buildVar([...idxPath, idx + 1])},`));
                lines.push(`};`, "");
            }
            const compPtr = node.type === "UI_COMP_SLIDER" ? `&${_var}_slider` :
                node.type === "UI_COMP_SELECTOR" ? `&${_var}_selector` :
                    node.type === "UI_COMP_FILE_BROWSER" ? `&${_var}_fb` : `NULL`;
            const handlerPtr = node.handler || `NULL`;
            const parentPtr = idxPath.length === 1 ? `NULL` : `&${buildVar(idxPath.slice(0, -1))}`;
            lines.push(`static const UiMenuItem ${_var}={`);
            lines.push(`    .name="${node.name}",`);
            lines.push(`    .comp_type=${node.type},`);
            lines.push(`    .comp=(void *)${compPtr},`);
            lines.push(`    .handler=${handlerPtr},`);
            lines.push(`    .parent=${parentPtr},`);
            lines.push(`    .child_num=${node.child_num},`);
            lines.push(`    .children=${node.child_num ? `${_var}_children` : `NULL`},`);
            lines.push(`};`, "");
            (node.children || []).forEach((ch, idx) => collectDefines(ch, [...idxPath, idx + 1], lines));
        }

        function collectInitLines(node, idxPath, initLines) {
            if ((node.type === "UI_COMP_SLIDER" || node.type === "UI_COMP_SELECTOR") && node.var_header && node.var_name) {
                const varStruct = node.type === "UI_COMP_SLIDER" ? `${buildVar(idxPath)}_slider_var` : `${buildVar(idxPath)}_selector_var`;
                initLines.push(`    ${varStruct}.current=${node.var_name};`);
            }
            (node.children || []).forEach((ch, idx) => collectInitLines(ch, [...idxPath, idx + 1], initLines));
        }

        function generateCFile(json) {
            (function fill(n) {
                n.children = n.children || [];
                n.child_num = n.children.length;
                n.children.forEach(fill);
            })(json);

            const lines = [];
            lines.push(`#include "${json.include}"`, "");
            const varHeaders = new Set();
            (function walk(n) {
                if ((n.type === "UI_COMP_SLIDER" || n.type === "UI_COMP_SELECTOR") && n.var_header)
                    varHeaders.add(n.var_header);
                (n.children || []).forEach(walk);
            })(json);
            varHeaders.forEach(h => lines.push(`#include "${h}"`));
            if (varHeaders.size) lines.push("");

            const handlers = new Set();
            (function walk(n) { if (n.handler && n.handler !== "NULL") handlers.add(n.handler); (n.children || []).forEach(walk); })(json);

            const decls = [], childrenDecls = [], sliderDecls = [], selectorDecls = [], fbDecls = [];
            collectDeclares(json, [1], decls, childrenDecls, sliderDecls, selectorDecls, fbDecls);

            if (decls.length) lines.push("/*********** 节点声明 ***********/", ...decls, "");
            if (childrenDecls.length) lines.push("/*********** 孩子声明 ***********/", ...childrenDecls, "");
            if (sliderDecls.length) lines.push("/*********** 滑条声明 ***********/", ...sliderDecls, "");
            if (selectorDecls.length) lines.push("/*********** 选择器声明 ***********/", ...selectorDecls, "");
            if (fbDecls.length) lines.push("/*********** 文件浏览器声明 ***********/", ...fbDecls, "");
            if (handlers.size) { lines.push("/*********** 外部回调 ***********/"); handlers.forEach(h => lines.push(`extern void ${h}(void *arg);`)); lines.push(""); }

            const defs = []; collectDefines(json, [1], defs); lines.push(...defs);

            const initLines = []; collectInitLines(json, [1], initLines);
            lines.push("/*********** 菜单数据更新 ***********/");
            varHeaders.forEach(h => lines.push(`#include "${h}"`));
            if (varHeaders.size) lines.push("");
            lines.push("void ui_menu_data_update(void)", "{");
            lines.push(...initLines);
            lines.push("}", "");
            return lines.join("\n");
        }

        /* ================= 合并生成函数 ================= */
        function generate_all() {
            function clean(node) {
                const n = { ...node };
                if (!n.handler) delete n.handler;
                if (!n.var_header) delete n.var_header;
                if (!n.var_name) delete n.var_name;
                if (n.children) n.children = n.children.map(clean);
                return n;
            }
            const data = { include: "Middlewares/Mini_UI/core/mini_ui.h", ...clean(root), child_num: root.children.length };
            generated_json = JSON.stringify(data, null, 2);
            generated_c_code = generateCFile(data);
            document.getElementById("jsonPreview").value = generated_json;
            document.getElementById("cPreview").value = generated_c_code;
        }

        function copy_code(type) {
            const ta = type === 'json' ? document.getElementById("jsonPreview") : document.getElementById("cPreview");
            ta.select(); ta.setSelectionRange(0, 99999);
            document.execCommand("copy");
            const tip = document.getElementById("copyTip");
            tip.textContent = "已复制" + (type === 'json' ? "JSON" : "C代码") + "!";
            tip.classList.add("show");
            setTimeout(() => tip.classList.remove("show"), 1200);
        }

        function extract_chars() {
            const names = [];

            function collect_names(node) {
                names.push(node.name);

                if (node.type === "UI_COMP_SELECTOR" && node.comp_data && node.comp_data.options) {
                    node.comp_data.options.forEach(ch => names.push(ch));
                }

                if (node.children) node.children.forEach(collect_names);
            }
            collect_names(root);

            const allText = names.join('');
            const chinese = [...new Set(allText.match(/[\u4e00-\u9fff]/g))].join('');
            const ascii = [...new Set(allText.match(/[a-zA-Z0-9]/g))].join('');

            document.getElementById('used_chinese').textContent = chinese || '无';
            document.getElementById('used_ascii').textContent = ascii || '无';
        }

        function copy_chinese() {
            const text = document.getElementById("used_chinese").textContent;
            if (!text || text === '无') return alert("没有汉字可复制");
            navigator.clipboard.writeText(text);
            alert("已复制汉字：" + text);
        }

        function copy_ascii() {
            const text = document.getElementById("used_ascii").textContent;
            if (!text || text === '无') return alert("没有英文字符可复制");
            navigator.clipboard.writeText(text);
            alert("已复制英文字符：" + text);
        }
        /* ================= 初始化 ================= */
        rerender();
    </script>
</body>

</html>